import { Callout } from 'nextra/components'
import BadgeGroup, { UniverTypes } from '@/components/BadgeGroup'

# 通用 API

<BadgeGroup values={[UniverTypes.GENERAL]} value={UniverTypes.GENERAL} />

在 Univer 中，根据文档的不同类型，所能调用的 Facade API 也有所不同。本章节将介绍在所有类型文档都适用的通用 Facade API。

## 概念

### 命令

Univer 中大多数的操作都会注册到命令系统，并通过命令系统来触发。这种统一的操作方式使得 Univer 可以很容易的实现撤销、重做、协同等功能。

<Callout type="info" emoji="ℹ️">
  如需了解更多设计细节请阅读 [Univer 命令系统](/guides/sheet/architecture/univer#命令系统) 。
</Callout>

## 命令系统

Univer 为用户提供了统一的命令系统，通过命令系统用户可以实现各种定制化的功能。

<Callout type="info" emoji="ℹ️">
  每一个命令对应了一个唯一的 ID。如果你正在寻找某个特定的命令 ID，可以参考 [如何查找命令 ID](/guides/sheet/tutorials/find-the-command-id)。
</Callout>

### 监听命令

Univer 设计了两种监听命令的方式，分别是在命令执行前和在命令执行后：

- `onBeforeCommandExecute`：在命令执行前执行自定义逻辑。
- `onCommandExecuted`：在命令执行后执行自定义逻辑。

#### 监听命令执行前

在命令执行之前，我们向 `FUniver.onBeforeCommandExecute` API 传入一个回调函数来注册自定义的预处理监听器。

当命令执行前，预处理监听器内逻辑会被执行。

```typescript
const univerAPI = FUniver.newAPI(univer);

univerAPI.onBeforeCommandExecute((command)=>{
  const { id, type, params } = command;
  // 在命令执行前执行自定义逻辑
})
```

如果你想在命令执行前阻止命令执行，可以在监听器回调中 `throw` 一个异常。

```typescript
const univerAPI = FUniver.newAPI(univer);

univerAPI.onBeforeCommandExecute((command)=>{
  throw new Error('禁止编辑')
})
```

#### 监听命令执行后

在命令执行之后，我们也可以向 `FUniver.onCommandExecuted` API 传入一个回调函数来注册自定义的后处理监听器。

当命令执行后，后处理监听器内逻辑会被执行。

```typescript
const univerAPI = FUniver.newAPI(univer);

univerAPI.onCommandExecuted((command)=>{
  const { id, type, params } = command;
  // 在命令执行后执行自定义逻辑
})
```

### 取消监听

注册命令监听器的方法会返回一个 `IDisposable` 对象，调用 `IDisposable.dispose` 可以销毁该监听器。

建议你及时销毁不再使用的监听器，有助于提高程序的健壮性。

```typescript
const univerAPI = FUniver.newAPI(univer);

// 注册监听器
const disposable = univerAPI.onBeforeCommandExecute((command)=>{
  // 在命令执行前执行自定义逻辑
})

// 示例：1 秒后取消监听
setTimeout(()=>{
  // 取消监听
  disposable.dispose();
}, 1000);
```

### 执行命令

如果你知道命令 ID 和所需要传递的参数，也可以通过 `FUniver.executeCommand` 方法来执行命令。

例如，我们可以通过 `sheet.command.set-range-values` 命令来设置单元格 A1 的值：

```typescript
const univerAPI = FUniver.newAPI(univer);

// 执行命令
univerAPI.executeCommand('sheet.command.set-range-values', {
  value: { v: "Hello, Univer!" },
  range: { startRow: 0, startColumn: 0, endRow: 0, endColumn: 0 }
});
```

## 撤销和重做

### 撤销

```ts
univerAPI.executeCommand('univer.command.undo')
```

### 重做

```ts
univerAPI.executeCommand('univer.command.redo')
```

## UI

请参考下面文档来拓展 Univer 的 UI :

- [扩展 Canvas](/guides/customization/canvas/)
- [自定义主题](/guides/customization/theme/)

## WebSocket

Facade 提供了一个便捷的 API `createSocket` 来创建 WebSocket，传入一个 URL 即可。
然后可以监听 open、message、close、error 事件，以及主动发送消息 send 方法和主动关闭 close 方法。

```typescript
// URL 换成你自己 WebSocket 服务的地址
const ws = univerAPI.createSocket("ws://47.100.177.253:8449/ws");

ws.open$.subscribe(() => {
  console.log('websocket opened')
  ws.send('hello')
})

ws.message$.subscribe((message) => {
  console.log('websocket message', message)
  const content = JSON.parse(message.data).content
  if (!content.includes('command')) {
    return
  }

  const commandInfo = JSON.parse(content);
  const { command, options } = commandInfo;
  const { id, params } = command;

  // 接受到协同数据，本地落盘
  univerAPI.executeCommand(id, params, options)
});

ws.close$.subscribe(() => {
  console.log("websocket closed");
});

ws.error$.subscribe((error) => {
  console.log("websocket error", error);
});

univerAPI.onCommandExecuted((command, options) => {
  // 仅同步本地 mutation
  if (command.type !== 2 || options?.fromCollab || options?.onlyLocal || command.id === 'doc.mutation.rich-text-editing') {
    return;
  }

  const commandInfo = JSON.stringify({ command, options: { fromCollab: true } })
  ws.send(commandInfo);
})
```

注意：启动 Univer 的时候要确保有 unitID ，不指定 unitID 的话无法协同。

## 注册公式

使用 Facade API，可以方便快速的在当前 Univer 实例中注册自定义公式。

如下案例所示，使用 `registerFunction` 将一个 `CUSTOMSUM` 公式所需要的算法、名称、描述一次性注册到公式插件，执行之后就可以使用公式了。在任一空白单元格输入 `=CUSTOMSUM` 可以看到提示。

```typescript
univerAPI.registerFunction({
  calculate: [
    [function (...variants) {
      let sum = 0;

      for(const variant of variants){
        sum += Number(variant) || 0;
      }

      return sum;
    }, 'CUSTOMSUM', '求参数的和'],
    // ... 更多公式
  ]
})
```

如果需要卸载所注册的公式，可以调用 `dispose` 方法。

```typescript
const functionDisposable = univerAPI.registerFunction({
   // calculate
})

// 卸载所注册的公式
functionDisposable.dispose();
```

如果想要提供更完善的国际化内容和描述，还可以配置 `locales` 和 `description` 字段。如下所示。

```typescript
const FUNCTION_NAMES_USER = {
  CUSTOMSUM: 'CUSTOMSUM'
}

univerAPI.registerFunction({
  locales: {
    'zhCN': {
      formulaCustom: {
        CUSTOMSUM: {
          description: '将单个值、单元格引用或是区域相加，或者将三者的组合相加。',
          abstract: '求参数的和',
          links: [
            {
              title: '教学',
              url: 'https://support.microsoft.com/zh-cn/office/sum-%E5%87%BD%E6%95%B0-043e1c7d-7726-4e80-8f32-07b23e057f89',
            },
          ],
          functionParameter: {
            number1: {
              name: '数值1',
              detail: '要相加的第一个数字。 该数字可以是 4 之类的数字，B6 之类的单元格引用或 B2:B8 之类的单元格范围。',
            },
            number2: {
              name: '数值2',
              detail: '这是要相加的第二个数字。 可以按照这种方式最多指定 255 个数字。',
            },
          },
        },
        // ... 更多公式
      },
    },
    'enUS': {
      formulaCustom: {
        CUSTOMSUM: {
          description: `You can add individual values, cell references or ranges or a mix of all three.`,
          abstract: `Adds its arguments`,
          links: [
            {
              title: 'Instruction',
              url: 'https://support.microsoft.com/en-us/office/sum-function-043e1c7d-7726-4e80-8f32-07b23e057f89',
            },
          ],
          functionParameter: {
            number1: {
              name: 'number1',
              detail: 'The first number you want to add. The number can be like 4, a cell reference like B6, or a cell range like B2:B8.',
            },
            number2: {
              name: 'number2',
              detail: 'This is the second number you want to add. You can specify up to 255 numbers in this way.',
            },
          },
        },
      }
    }
  },
  description: [
    {
      functionName: FUNCTION_NAMES_USER.CUSTOMSUM,
      aliasFunctionName: 'formulaCustom.CUSTOMSUM.aliasFunctionName',
      functionType: 15,
      description: 'formulaCustom.CUSTOMSUM.description',
      abstract: 'formulaCustom.CUSTOMSUM.abstract',
      functionParameter: [
        {
          name: 'formulaCustom.CUSTOMSUM.functionParameter.number1.name',
          detail: 'formulaCustom.CUSTOMSUM.functionParameter.number1.detail',
          example: 'A1:A20',
          require: 1,
          repeat: 0,
        },
        {
          name: 'formulaCustom.CUSTOMSUM.functionParameter.number2.name',
          detail: 'formulaCustom.CUSTOMSUM.functionParameter.number2.detail',
          example: 'B2:B10',
          require: 0,
          repeat: 1,
        },
      ],
    },
    // ... 更多公式
  ],
  calculate: [
    [function (...variants) {
      let sum = 0;

      for (const variant of variants) {
        sum += Number(variant) || 0;
      }

      return sum;
    }, FUNCTION_NAMES_USER.CUSTOMSUM],
    // ... 更多公式
  ]
})
```

说明

- `locales` 下可以设置多种语言，命名规则参考 [LocaleType](/typedoc/@univerjs/core/enumerations/LocaleType)。可以在 `functionList` 下添加多个公式的翻译。详细的字段说明请参考 [如何在 Formula Engine 中添加公式](/guides/sheet/advanced/custom-formula/#如何在-formula-engine-中添加公式) 的部分。
- `description` 设置自定义公式的描述。
- `calculate` 编写计算公式的具体算法和名称映射。入参为使用公式时用户输入的内容，可能为数字、字符串、布尔值，或者一个数组。

<Callout type="info" emoji="ℹ️">
  如果想复用公式系统提供的算法，增强公式算法的能力，可以通过插件配置的方式注册自定义公式，详细教程请参考 [自定义公式](/guides/sheet/advanced/custom-formula/)。
</Callout>
